/*
 * Created on 27 Mar 2007 by Andrew White
 */

package com.volatileshooter.networking.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.InvalidPropertiesFormatException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Properties;

import com.volatileshooter.networking.io.GameChannel;
import com.volatileshooter.networking.io.GamePacket;
import com.volatileshooter.networking.io.packets.AcceptPacket;
import com.volatileshooter.networking.io.packets.ChatPacket;
import com.volatileshooter.networking.io.packets.JoinPacket;
import com.volatileshooter.networking.io.packets.QuitPacket;
import com.volatileshooter.networking.io.packets.RejectPacket;
import com.volatileshooter.networking.server.client.Client;
import com.volatileshooter.networking.server.client.ClientHandler;
import com.volatileshooter.networking.server.util.Log;

public class Server {

    /* max time in milliseconds to wait to read a packet */
    public final static int MAX_PACKET_WAIT = 100;

    /* property objects */
    private String name;

    private String message;

    private int port;

    private Object map_list[];

    private short max_players;

    private int map_cycle;

    private String log_path;

    /* nio */
    private DatagramChannel udpChannel;

    private Selector selector;

    /* game channel */
    private GameChannel gameChannel;

    /* client handler */
    private ClientHandler clientHandler;

    /* application run */
    private boolean isRunning = true;

    /**
     * Constructs new instance of server. Properties are loaded from the
     * properties xml file provided as a command line argument. A log is started
     * which outputs to the file specified in the properties file.
     * 
     * @param properties_path
     */
    public Server(String properties_path) {

        // start logging
        new Log("cyber.server");

        // start server
        Log.info("\nStarting Cyber Server...\n");

        // load properties from xml file
        Properties properties = new Properties();
        try {
            properties.loadFromXML(new FileInputStream(
                    new File(properties_path)));
        } catch (FileNotFoundException e) {
            Log.warning("Properties file not found", e);
        } catch (InvalidPropertiesFormatException e) {
            Log.warning("Properties file corrupt", e);
        } catch (IOException e) {
            Log.warning("Failed to load properties file", e);
        }

        // get properties, if an error occurred whilst loading the properties
        // file then the default values are used
        name = properties.getProperty("name", "Default Server");
        message = properties.getProperty("message",
                "Ahmed touches little boys!");
        try {
            port = Integer.parseInt(properties.getProperty("port", "27015"));
        } catch (NumberFormatException e) {
            Log
                    .warning(
                            "Failed to read port number from properties file, using default",
                            null);
            port = 27015;
        }
        map_list = new Object[] { "map1.xml", "map2.xml", "map3.xml" };
        try {
            map_cycle = Integer.parseInt(properties.getProperty("map_cycle",
                    "-1"));
        } catch (NumberFormatException e) {
            Log
                    .warning(
                            "Failed to read map cycle number from properties file, using default",
                            null);
            map_cycle = -1;
        }
        try {
            max_players = Short.parseShort(properties.getProperty(
                    "max_players", "6"));
        } catch (NumberFormatException e) {
            Log
                    .warning(
                            "Failed to read max number of players from properties file, using default",
                            null);
            max_players = 6;
        }
        log_path = properties.getProperty("log_path", "server%g[id%u].log");

        // start logging to file
        try {
            Log.setLogFile(log_path);
        } catch (SecurityException e) {
            Log.warning("Logging to [" + log_path
                    + "] could not begin due to security reasons", e);
        } catch (IOException e) {
            Log.warning("Failed to start logging to [" + log_path + "]", e);
        }
    }

    /**
     * Initializes server by opening up UDP channel.
     */
    private void init() {
        // log
        Log.info("\n##### Cyber Server #####\n");

        // open UDP channel, in non-blocking mode, bound to local IP and port
        try {
            selector = Selector.open();
            udpChannel = DatagramChannel.open();
            udpChannel.configureBlocking(false);
            udpChannel.socket().bind(
                    new InetSocketAddress(InetAddress.getLocalHost(), port));
            udpChannel.register(selector, SelectionKey.OP_READ);
        } catch (IOException e) {
            Log.error("Failed to start server", e);
        }
        Log.info("Server live on: "
                + udpChannel.socket().getLocalAddress().getHostAddress() + ":"
                + udpChannel.socket().getLocalPort());

        // game channel for reading and writing packets
        gameChannel = new GameChannel(udpChannel);

        // create new client handler
        clientHandler = new ClientHandler(max_players);
    }

    /**
     * Reads in packets from channel buffer.
     */
    private void update() {
        try {
            // get selected keys (block temporarily if no packets to read)
            selector.select(MAX_PACKET_WAIT);

            // loop through any keys
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                it.remove();

                // check if the key is readable
                if (key.isReadable()) {

                    // read packet from channel
                    GamePacket packet = gameChannel.readPacket();

                    // check if a packet was returned
                    if (packet != null) {

                        // packet received, check if join packet
                        if (packet instanceof JoinPacket) {

                            // join request received from client
                            JoinPacket join = (JoinPacket) packet;

                            // attempt to add new client
                            Client client = clientHandler.add(join.getName(),
                                    (InetSocketAddress) join.addr);
                            // if client has been added
                            if (client != null) {
                                // send accept packet
                                AcceptPacket accept = new AcceptPacket(
                                        client.id, null, message);
                                clientHandler.sendPacketTo(client.id, accept,
                                        gameChannel);
                                // send player joined notification to other players
                                JoinPacket notification = new JoinPacket(
                                        client.id, null, join.getName());
                                clientHandler.sendPacketToAll(notification,
                                        gameChannel);
                                Log.info(join.getName() + " [" + join.addr
                                        + "] has joined the game!");
                            } else {
                                // reached max number of clients, send reject
                                // packet
                                gameChannel.sendPacket(new RejectPacket(
                                        ClientHandler.SERVER_ID, join.addr,
                                        "Server is full"));
                                Log
                                        .warning(
                                                "Player "
                                                        + join.getName()
                                                        + " ["
                                                        + ((InetSocketAddress) join.addr)
                                                                .getAddress()
                                                        + "] could not join as server has reached the maximum number of players ("
                                                        + max_players + ")",
                                                null);
                            }

                        } else {

                            // if the packet is not a join packet then it must
                            // be from
                            // an existing player, therefore do a security check
                            // to make
                            // sure the client Id matches the InetSocketAddress
                            Client client = clientHandler
                                    .getClient(packet.clientId);
                            
                            if (client == null) {
                                // packet received from a unknown player,
                                // therefore ignore it
                                Log.warning(
                                        "Ignoring packet received from unknown sender "
                                                + packet.addr, null);
                                return;
                            }
                            if (!client.addr.equals(packet.addr)) {
                                // packet received from a unknown player,
                                // therefore ignore it
                                Log.warning(
                                        "Ignoring packet received from unauthorised sender "
                                                + packet.addr, null);
                                return;
                            }

                            if (packet instanceof QuitPacket) {

                                // quit packet received
                                QuitPacket quit = (QuitPacket) packet;
                                // remove player from client list
                                clientHandler.remove(quit.clientId);
                                // send player quit notification
                                QuitPacket notification = new QuitPacket(
                                        client.id, null);
                                clientHandler.sendPacketToAll(notification,
                                        gameChannel);
                                Log.info(client.name
                                        + " has left the game!");

                            } else if (packet instanceof ChatPacket) {

                                // chat packet received
                                ChatPacket chat = (ChatPacket) packet;
                                // send chat to all clients
                                clientHandler
                                        .sendPacketToAll(chat, gameChannel);
                                // print chat to screen
                                Log.info("CHAT [" + chat.clientId + "]: "
                                        + chat.getChat());

                            } else {

                                // event type received that is not supported
                                // by server
                                Log.warning("Ignoring packet from "
                                        + packet.clientId + " [" + packet.addr
                                        + "], unknown event type "
                                        + packet.event, null);

                            }
                        }
                    }

                }
            }
        } catch (IOException e) {
            Log.warning("Error receiving packet", e);
        }
    }

    /**
     * Cleans up by closing connections, to be called before closing the server.
     */
    private void cleanup() {
        // close selector and UDP channel
        try {
            selector.close();
            udpChannel.close();
        } catch (IOException e) {
            Log.error("Failed to close UDP channel", e);
        }

    }

    /**
     * Main server loop.
     */
    public void run() {
        // initialise
        isRunning = true;
        init();
        // continuos loop
        while (isRunning) {
            // update
            update();
            // rest thread
            Thread.yield();
        }
        // cleanup before closing application
        cleanup();
    }

    /**
     * Exits the server cleanly, cleaning up before terminating.
     */
    public void exit() {
        isRunning = false;
    }

    /**
     * Starts new Server application.
     * 
     * @param args
     *            File path to properties files
     */
    public static void main(String[] args) {

        // set default blank property file path
        String path = "";

        // check if property file path provided
        if (args.length > 0) {
            path = args[args.length - 1];
        }

        // create a new server object, providing the property file path
        Server server = new Server(path);

        // run server (blocks)
        server.run();
    }

}
